/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.basics.bean.impl;

import java.io.IOException;
import java.io.InputStream;

/**
 * An {@link InputStream} that concats several {@link InputStream}.
 */
public class MergedInputStream
    extends InputStream
{

  /**
   * the {@link InputStream}s to concat.
   */
  private final transient InputStream[] inputStreams;

  /**
   * the index of the current {@link InputStream}.
   */
  private transient int index = 0;



  /**
   * constructor.
   * 
   * @param inputStreams
   *          the {@link InputStream}s to concat
   */
  public MergedInputStream(InputStream... inputStreams)
  {
    this.inputStreams = inputStreams;
  }



  /**
   * Returns the number of bytes that can be read (or skipped over) from this
   * input stream without blocking by the next caller of a method for this input
   * stream. The next caller might be the same thread or another thread.
   * 
   * @return the number of bytes that can be read from this input stream without
   *         blocking.
   * @exception IOException
   *              if an I/O error occurs.
   * @see java.io.InputStream#available()
   */
  @Override
  public int available()
      throws IOException
  {
    return currentInputStream().available();
  }



  /**
   * Closes this input stream and releases any system resources associated with
   * the stream.
   * 
   * @exception IOException
   *              if an I/O error occurs.
   * @see java.io.InputStream#close()
   */
  @Override
  public void close()
      throws IOException
  {
    while (hasStreams()) {
      currentInputStream().close();
      goToNextStream();
    }
  }



  /**
   * Reads the next byte of data from the input stream. The value byte is
   * returned as an <code>int</code> in the range <code>0</code> to
   * <code>255</code>. If no byte is available because the end of the stream
   * has been reached, the value <code>-1</code> is returned. This method
   * blocks until input data is available, the end of the stream is detected, or
   * an exception is thrown.
   * 
   * @return the next byte of data, or <code>-1</code> if the end of the
   *         stream is reached.
   * @exception IOException
   *              if an I/O error occurs.
   * @see java.io.InputStream#read()
   */
  @Override
  public int read()
      throws IOException
  {
    while (hasStreams()) {
      int currentChar = currentInputStream().read();
      if (currentChar != -1) {
        return currentChar;
      }
      goToNextStream();
    }
    return -1;
  }



  /**
   * Reads up to <code>len</code> bytes of data from the input stream into an
   * array of bytes. An attempt is made to read as many as <code>len</code>
   * bytes, but a smaller number may be read. The number of bytes actually read
   * is returned as an integer.
   * <p>
   * This method blocks until input data is available, end of file is detected,
   * or an exception is thrown.
   * <p>
   * If <code>b</code> is <code>null</code>, a
   * <code>NullPointerException</code> is thrown.
   * <p>
   * If <code>off</code> is negative, or <code>len</code> is negative, or
   * <code>off+len</code> is greater than the length of the array
   * <code>b</code>, then an <code>IndexOutOfBoundsException</code> is
   * thrown.
   * <p>
   * If <code>len</code> is zero, then no bytes are read and <code>0</code>
   * is returned; otherwise, there is an attempt to read at least one byte. If
   * no byte is available because the stream is at end of file, the value
   * <code>-1</code> is returned; otherwise, at least one byte is read and
   * stored into <code>b</code>.
   * <p>
   * The first byte read is stored into element <code>b[off]</code>, the next
   * one into <code>b[off+1]</code>, and so on. The number of bytes read is,
   * at most, equal to <code>len</code>. Let <i>k</i> be the number of bytes
   * actually read; these bytes will be stored in elements <code>b[off]</code>
   * through <code>b[off+</code><i>k</i><code>-1]</code>, leaving
   * elements <code>b[off+</code><i>k</i><code>]</code> through
   * <code>b[off+len-1]</code> unaffected.
   * <p>
   * In every case, elements <code>b[0]</code> through <code>b[off]</code>
   * and elements <code>b[off+len]</code> through <code>b[b.length-1]</code>
   * are unaffected.
   * <p>
   * If the first byte cannot be read for any reason other than end of file,
   * then an <code>IOException</code> is thrown. In particular, an
   * <code>IOException</code> is thrown if the input stream has been closed.
   * 
   * @param byteArray
   *          the buffer into which the data is read.
   * @param off
   *          the start offset in array <code>b</code> at which the data is
   *          written.
   * @param len
   *          the maximum number of bytes to read.
   * @return the total number of bytes read into the buffer, or <code>-1</code>
   *         if there is no more data because the end of the stream has been
   *         reached.
   * @exception IOException
   *              if an I/O error occurs.
   * @see java.io.InputStream#read(byte[], int, int)
   */
  @Override
  public int read(byte[] byteArray, int off, int len)
      throws IOException
  {
    while (hasStreams()) {
      int res = currentInputStream().read(byteArray, off, len);
      if (res >= 0) {
        return res;
      }
      goToNextStream();
    }
    return -1;
  }



  /**
   * Skips over and discards <code>n</code> bytes of data from this input
   * stream. The <code>skip</code> method may, for a variety of reasons, end
   * up skipping over some smaller number of bytes, possibly <code>0</code>.
   * This may result from any of a number of conditions; reaching end of file
   * before <code>n</code> bytes have been skipped is only one possibility.
   * The actual number of bytes skipped is returned. If <code>n</code> is
   * negative, no bytes are skipped.
   * 
   * @param numberToSkip
   *          the number of bytes to be skipped.
   * @return the actual number of bytes skipped.
   * @exception IOException
   *              if an I/O error occurs.
   * @see java.io.InputStream#skip(long)
   */
  @Override
  public long skip(long numberToSkip)
      throws IOException
  {
    if (!hasStreams()) {
      return 0;
    }
    long res = currentInputStream().skip(numberToSkip);
    if (res > 0) {
      return res;
    }
    if (read() != -1) {
      return 1;
    }
    return 0;
  }



  /**
   * returns the current {@link InputStream}.
   * 
   * @return the current {@link InputStream}
   */
  private InputStream currentInputStream()
  {
    return inputStreams[index];
  }



  /**
   * update the current {@link InputStream} to the next one in queue.
   * 
   * @throws IOException
   *           if an error occurred while closing a finished {@link InputStream}
   */
  private void goToNextStream()
      throws IOException
  {
    currentInputStream().close();
    index++;
  }



  /**
   * returns whether there is still data to read on an {@link InputStream}.
   * 
   * @return whether there is still data to read on an {@link InputStream}
   */
  private boolean hasStreams()
  {
    return index < inputStreams.length;
  }
}
